﻿using Bit.Art.Core.Extensions;
using Bit.Art.Core.Models;
using Bit.Art.Db;
using Bit.Art.Distributed;
using NetMQ;
using NetMQ.Sockets;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System.IO;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using System.Linq;
using Bit.Art.Core;
using System.Threading;
using Serilog;
using Serilog.Events;
using Serilog.Sinks.SystemConsole.Themes;

namespace Bit.Art.MySqlDb
{
    public class Program
    {
        private static volatile bool s_Runing = true;
        internal static IConfigurationRoot Configuration { get; set; }
        public static IServiceProvider GlobalServiceProvider { get; set; }
        private static ReaderWriterLockSlim _globalConfigChangeLock = new ReaderWriterLockSlim();
        // 默认的进入timeout的时间是1秒
        private const int READ_WRITE_LOCK_TIMEOUT_VALUE = 1000;
        private static long _orderNo;
        private static string[] s_Markets;
        private static Task[] s_Tasks;

#if UNITTEST
        public static AutoResetEvent s_autoResetEventForTest { get; private set; } = new AutoResetEvent(false);
#endif

        private static void InitializeConfiguration()
        {
            var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
            var builder = new ConfigurationBuilder().SetBasePath(Directory.GetCurrentDirectory())
                                                    .AddJsonFile("appsettings.json");
            if (!string.IsNullOrWhiteSpace(environment))
                builder = builder.AddJsonFile($"appsettings.{environment}.json");
            
            Configuration = builder.Build();
            ApplicationLogging.LoggerFactory.AddConsole(Configuration.GetSection("Logging"));
        }

        public static void Setup()
        {
            InitializeConfiguration();
            var services = new ServiceCollection();
            services.AddDbContext<ExchangeConfigDb>(options =>
                options.UseMySql(Configuration.GetConnectionString("ExchangeConfigDb")), ServiceLifetime.Transient);
            services.AddDbContext<ExchangeDb>(options =>
                options.UseMySql(Configuration.GetConnectionString("ExchangeDb")), ServiceLifetime.Transient);
            GlobalServiceProvider = services
                .AddLogging()
                .BuildServiceProvider();
        }
        
        static void Main(string[] args)
        {
            Console.WriteLine("====== DB PERSIST ======");
            Setup();
            s_Tasks = StartEngine();
            Task.WaitAll(s_Tasks);
        }

        public static void StopEngine()
        {
            s_Runing = false;
            foreach (var task in s_Tasks)
            {
                task.Wait(1000);
                task.Dispose();
            }
        }

        public static Task[] StartEngine()
        {
            Log.Logger = new LoggerConfiguration()
                .MinimumLevel.Debug()
                .MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
                .MinimumLevel.Override("System", LogEventLevel.Warning)
                .MinimumLevel.Override("Microsoft.AspNetCore.Authentication", LogEventLevel.Information)
                .Enrich.FromLogContext()
                .WriteTo.File("mysql.log", rollingInterval: RollingInterval.Day)
                .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level}] {SourceContext}{NewLine}{Message:lj}{NewLine}{Exception}{NewLine}", theme: AnsiConsoleTheme.Literate)
                .CreateLogger();
            
            var tasks = new List<Task>();

            tasks.Add(DealWithDalanceTask());

            tasks.Add(DealWithNewOrderTask());

            return tasks.ToArray();
        }

        /// <summary>
        /// 处理用户余额Task
        /// </summary>
        /// <returns></returns>
        private static Task DealWithDalanceTask()
        {
            return Task.Run(() =>
            {
                try
                {
                    using (var db = GlobalServiceProvider.GetService<ExchangeDb>())
                    {
                        using (var response = new ResponseSocket(string.Format("@{0}", SiteConfiguration.DatabaseEndpoint)))
                        {
#if UNITTEST
                            s_autoResetEventForTest.Set();
#endif
                            while (s_Runing)
                            {
                                string resultJson = null;
                                try
                                {
                                    var msg = response.ReceiveMultipartMessage();
                                    // ascii编码应该足够了
                                    var json = msg.Last.ConvertToString();
                                    var dmb = Utility.From<DataMessageBase>(json);

                                    switch (dmb.Type)
                                    {
                                        case (int)DataMessageBase.MessageType.UpdateBalance:
                                            resultJson = DoUpdateBalance(json, db);
                                            break;

                                        case (int)DataMessageBase.MessageType.GetOrderDetails:
                                            resultJson = DoGetOrderDetails(json, db);
                                            break;

                                        case (int)DataMessageBase.MessageType.UpdateMarket:
                                            resultJson = DoUpdateMarket();
                                            break;
                                    }
                                }
                                catch (Exception e)
                                {
                                    var result = new ApiCallResult();
                                    result.ErrorCode = Constants.ErrorCodes.SERVER_ERROR;
                                    result.Message = e.Message;
                                    resultJson = result.ToJson();
                                    Log.Logger.Error(e, "mysql节点更新用户余额到数据库时出错！");
#if UNITTEST
                                    s_autoResetEventForTest.Set();
#endif
                                }

                                response.SendFrame(resultJson);

#if UNITTEST
                                s_autoResetEventForTest.Set();
#endif
                            }
                        }
                    }
                }
                catch (Exception e)
                {
                    Log.Logger.Error(e, "mysql节点处理用户余额更新节点出错！");
#if UNITTEST
                    s_autoResetEventForTest.Set();
#endif
                }
            });
        }

        /// <summary>
        /// 处理新订单
        /// </summary>
        /// <returns></returns>
        private static Task DealWithNewOrderTask()
        {
            return Task.Run(() =>
            {
                try
                {
                    DoUpdateMarket();

                    using (var db = GlobalServiceProvider.GetService<ExchangeDb>())
                    {
                        using (var puller = new ResponseSocket(string.Format(">{0}", SiteConfiguration.BackEndpoint)))
                        {
                            using (var ordermatcher = new PushSocket(string.Format(">{0}", SiteConfiguration.OrderMatchEndpoint)))
                            {
#if UNITTEST
                                s_autoResetEventForTest.Set();
#endif

                                while (s_Runing)
                                {
                                    try
                                    {
                                        var msg = puller.ReceiveMultipartMessage();
                                        var order = Order.FromBytes(msg.Last.Buffer);
                                        Log.Logger.Debug($"[mysqldb]获得新订单{order.Volume}");
                                        System.Threading.Interlocked.Increment(ref _orderNo);

                                        var market = order.Market.ToUpper();
                                        var marketIdx = -1;

                                        try
                                        {
                                            if (_globalConfigChangeLock.TryEnterReadLock(READ_WRITE_LOCK_TIMEOUT_VALUE))
                                            {
                                                marketIdx = Array.BinarySearch(s_Markets, market);
                                            }
                                        }
                                        finally
                                        {
                                            _globalConfigChangeLock.ExitReadLock();
                                        }

                                        if (marketIdx < 0)
                                        {
                                            puller.SendFrame(Constants.MARKET_NOT_ENABLED.ToByteArray());
                                            continue;
                                        }

                                        if (Utility.SplitMarket(order.Market, out string quote, out string coin))
                                        {
                                            // 根据Bug：http://gitlab.vikvon.com/shiyimin/1707/issues/5 的描述
                                            // 目前采用 REQ-REP 模式需要接收端回复后才能接收下一个消息，因此在目前使用REQ-REP模式下，尽早回复消息是一个提高性能的技巧
                                            Log.Logger.Debug($"[mysqldb] 推送订单到撮合引擎并保存, no: {_orderNo}, price: {order.Price}, volume: {order.Volume}, side: {order.Side}");
                                            ordermatcher.SendMultipartMessage(msg);
                                            // TODO: 需要优化订单保存到数据库的逻辑
                                            // 1. Add改成直接用INSERT语句拼接，Add好像会加载不少的数据库数据
                                            // 2. SaveChanges是同步的，需要改成异步的保存
                                            // 3. 应该定期批量保存order，而不是一个个order保存
                                            order.PlacedDate = DateTime.Now;
                                            db.Database.ExecuteSqlCommand("INSERT INTO Orders (Id, Market, OrderType, PlacedDate, Price," +
                                                                          "                    Side, UserId, VipLevel, Volume, LockedBalance, QuoteVolumeDealed, QuoteVolume, VolumeRemaining, IsCancelled, Taker) " +
                                                                          "VALUES (@p0, @p1, @p2, @p3, @p4, @p5, @p6, @p7, @p8, @p9, 0, 0, 0, 0, 0)",
                                                                          order.Id, order.Market, order.OrderType, order.PlacedDate, order.Price,
                                                                          order.Side, order.UserId, order.VipLevel, order.Volume, order.LockedBalance);

                                            puller.SendFrame(order.Id.ToByteArray());
                                            var bid = order.Side == (int)TradeSide.Buy;
                                            decimal lockedQuote = -1, lockedCoin = -1;

                                            if (bid && order.OrderType == (int)OrderType.Limit)
                                            {
                                                lockedQuote = order.Price.Value * order.Volume * (1 + order.GetFeeRate());
                                                db.Database.ExecuteSqlCommand("UPDATE AccountBalances SET Pending = Pending + @p0 WHERE AccountId = @p1 AND Currency = @p2;",
                                                    lockedQuote, order.UserId, quote);
                                            }
                                            else if (!bid && order.OrderType == (int)OrderType.Limit)
                                            {
                                                lockedCoin = order.Volume;
                                                db.Database.ExecuteSqlCommand("UPDATE AccountBalances SET Pending = Pending + @p0 WHERE AccountId = @p1 AND Currency = @p2;",
                                                    lockedCoin, order.UserId, coin);
                                            }
                                        }
                                        else
                                        {
                                            puller.SendFrame(Constants.MARKET_FORMAT_ERROR.ToByteArray());
                                        }
                                    }
                                    catch (Exception e)
                                    {
                                        puller.SendFrame(Constants.SERVER_ERROR.ToByteArray());
                                        Log.Logger.Error(e, "mysql节点处理新订单失败！");
                                    }
#if UNITTEST
                                    s_autoResetEventForTest.Set();
#endif
                                }
                            }
                        }
                    }
                }
                catch (Exception e)
                {
                    Log.Logger.Error(e, "启动mysqldb节点失败！");
                }
            });
        }

        private static string DoUpdateMarket()
        {
            var result = new ApiCallResult();

            try
            {
                using (var db = GlobalServiceProvider.GetService<ExchangeConfigDb>())
                {
                    try
                    {
                        if (_globalConfigChangeLock.TryEnterWriteLock(READ_WRITE_LOCK_TIMEOUT_VALUE))
                        {
                            s_Markets = db.Markets.Where(m => m.IsEnabled).Select(m => m.Name).OrderBy(m => m).ToArray();
                        }
                        else
                        {
                            // TODO: 需要增加万一Market获取不到读写锁情况下的处理代码
                            Log.Logger.Warning("由于无法获取全局配置读写锁的原因，无法更新交易对信息！");
                        }
                    }
                    finally
                    {
                        _globalConfigChangeLock.ExitWriteLock();
                    }
                }

                result.Success = true;
            }
            catch (Exception e)
            {
                result.Message = e.Message;
            }

            return result.ToJson();
        }

        private static string DoGetOrderDetails(string json, ExchangeDb db)
        {
            var result = new ApiCallResult<ExchangeOrderInfo>();
            try
            {
                var godm = Utility.From<GetOrderDetailsMessage>(json);
                if (Guid.TryParse(godm.OrderId, out Guid oid))
                {
                    var order = db.Orders.SingleOrDefault(o => o.Id == oid && o.UserId == godm.UserId);
                    if (order != null)
                    {
                        result.Success = true;
                        result.Result = new ExchangeOrderInfo()
                        {
                            Market = godm.Market,
                            Price = order.Price,
                            Quantity = order.Volume,
                            QuantityRemaining = order.VolumeRemaining,
                            Side = order.Side == 0 ? TradeSide.Buy : TradeSide.Sell,
                            PlacedTimestamp = order.PlacedDate
                        };
                    }
                    else
                    {
                        result.Message = string.Format($"找不到用户名下订单号为{godm.OrderId}的订单");
                    }
                }
                else
                {
                    result.Message = string.Format($"无法解析订单号{godm.OrderId}");
                }
            }
            catch (Exception e)
            {
                Log.Logger.Error(e, string.Format("处理获取订单请求{json}出错！"));
            }

            return result.ToJson();
        }

        private static string DoUpdateBalance(string json, ExchangeDb db)
        {
            var ubm = Utility.From<UpdateBalanceMessage>(json);
            var affected = db.Database.ExecuteSqlCommand("UPDATE AccountBalances SET Balance = Balance + @p0 WHERE AccountId = @p1 AND Currency = @p2;",
                ubm.Amount, ubm.UserId, ubm.Coin);
                                                         
            if (affected == 0)
            {
                affected = db.Database.ExecuteSqlCommand("INSERT INTO AccountBalances (Id, AccountId, Currency, Balance, Pending) VALUES (@p0, @p1, @p2, @p3, @p4)",
                                                         Guid.NewGuid(), ubm.UserId, ubm.Coin, ubm.Amount, 0);
            }

            var result = new ApiCallResult();
            result.Success = affected > 0;
            if (!result.Success)
            {
                result.Message = string.Format($"无法给用户{ubm.UserId}更新或新增币种{ubm.Coin}的余额！");
            }

            return result.ToJson();
        }
    }
}
